(function() {


    var rsplit = function(string, regex) {
        var result = regex.exec(string), retArr = new Array(), first_idx, last_idx, first_bit;
        while (result != null)
        {
            first_idx = result.index;
            last_idx = regex.lastIndex;
            if ((first_idx) != 0)
            {
                first_bit = string.substring(0, first_idx);
                retArr.push(string.substring(0, first_idx));
                string = string.slice(first_idx);
            }
            retArr.push(result[0]);
            string = string.slice(result[0].length);
            result = regex.exec(string);
        }
        if (!string == '')
        {
            retArr.push(string);
        }
        return retArr;
    },
            chop = function(string) {
                return string.substr(0, string.length - 1);
            },
            extend = function(d, s) {
                for (var n in s) {
                    if (s.hasOwnProperty(n))
                        d[n] = s[n]
                }
            };


    EJS = function(options) {
        options = typeof options == "string" ? {view: options} : options
        this.set_options(options);
        if (options.precompiled) {
            this.template = {};
            this.template.process = options.precompiled;
            EJS.update(this.name, this);
            return;
        }
        if (options.element)
        {
            if (typeof options.element == 'string') {
                var name = options.element
                options.element = document.getElementById(options.element)
                if (options.element == null)
                    throw name + 'does not exist!'
            }
            if (options.element.value) {
                this.text = options.element.value
            } else {
                this.text = options.element.innerHTML
            }
            this.name = options.element.id
            this.type = '['
        } else if (options.url) {
            options.url = EJS.endExt(options.url, this.extMatch);
            this.name = this.name ? this.name : options.url;
            var url = options.url
            //options.view = options.absolute_url || options.view || options.;
            var template = EJS.get(this.name /*url*/, this.cache);
            if (template)
                return template;
            if (template == EJS.INVALID_PATH)
                return null;
            try {
                this.text = EJS.request(url + (this.cache ? '' : '?' + Math.random()));
            } catch (e) {
            }

            if (this.text == null) {
                throw({type: 'EJS', message: 'There is no template at ' + url});
            }
            //this.name = url;
        }
        var template = new EJS.Compiler(this.text, this.type);

        template.compile(options, this.name);


        EJS.update(this.name, this);
        this.template = template;
    };
    /* @Prototype*/
    EJS.prototype = {
        /**
         * Renders an object with extra view helpers attached to the view.
         * @param {Object} object data to be rendered
         * @param {Object} extra_helpers an object with additonal view helpers
         * @return {String} returns the result of the string
         */
        render: function(object, extra_helpers) {
            object = object || {};
            this._extra_helpers = extra_helpers;
            var v = new EJS.Helpers(object, extra_helpers || {});
            return this.template.process.call(object, object, v);
        },
        update: function(element, options) {
            if (typeof element == 'string') {
                element = document.getElementById(element)
            }
            if (options == null) {
                _template = this;
                return function(object) {
                    EJS.prototype.update.call(_template, element, object)
                }
            }
            if (typeof options == 'string') {
                params = {}
                params.url = options
                _template = this;
                params.onComplete = function(request) {
                    var object = eval(request.responseText)
                    EJS.prototype.update.call(_template, element, object)
                }
                EJS.ajax_request(params)
            } else
            {
                element.innerHTML = this.render(options)
            }
        },
        out: function() {
            return this.template.out;
        },
        /**
         * Sets options on this view to be rendered with.
         * @param {Object} options
         */
        set_options: function(options) {
            this.type = options.type || EJS.type;
            this.cache = options.cache != null ? options.cache : EJS.cache;
            this.text = options.text || null;
            this.name = options.name || null;
            this.ext = options.ext || EJS.ext;
            this.extMatch = new RegExp(this.ext.replace(/\./, '\.'));
        }
    };
    EJS.endExt = function(path, match) {
        if (!path)
            return null;
        match.lastIndex = 0
        return path + (match.test(path) ? '' : this.ext)
    };




    /* @Static*/
    EJS.Scanner = function(source, left, right) {

        extend(this,
                {left_delimiter: left + '%',
                    right_delimiter: '%' + right,
                    double_left: left + '%%',
                    double_right: '%%' + right,
                    left_equal: left + '%=',
                    left_comment: left + '%#'})

        this.SplitRegexp = left == '[' ? /(\[%%)|(%%\])|(\[%=)|(\[%#)|(\[%)|(%\]\n)|(%\])|(\n)/ : new RegExp('(' + this.double_left + ')|(%%' + this.double_right + ')|(' + this.left_equal + ')|(' + this.left_comment + ')|(' + this.left_delimiter + ')|(' + this.right_delimiter + '\n)|(' + this.right_delimiter + ')|(\n)');

        this.source = source;
        this.stag = null;
        this.lines = 0;
    };

    EJS.Scanner.to_text = function(input) {
        if (input == null || input === undefined)
            return '';
        if (input instanceof Date)
            return input.toDateString();
        if (input.toString)
            return input.toString();
        return '';
    };

    EJS.Scanner.prototype = {
        scan: function(block) {
            scanline = this.scanline;
            regex = this.SplitRegexp;
            if (!this.source == '')
            {
                var source_split = rsplit(this.source, /\n/);
                for (var i = 0; i < source_split.length; i++) {
                    var item = source_split[i];
                    this.scanline(item, regex, block);
                }
            }
        },
        scanline: function(line, regex, block) {
            this.lines++;
            var line_split = rsplit(line, regex);
            for (var i = 0; i < line_split.length; i++) {
                var token = line_split[i];
                if (token != null) {
                    try {
                        block(token, this);
                    } catch (e) {
                        throw {type: 'EJS.Scanner', line: this.lines};
                    }
                }
            }
        }
    };


    EJS.Buffer = function(pre_cmd, post_cmd) {
        this.line = new Array();
        this.script = "";
        this.pre_cmd = pre_cmd;
        this.post_cmd = post_cmd;
        for (var i = 0; i < this.pre_cmd.length; i++)
        {
            this.push(pre_cmd[i]);
        }
    };
    EJS.Buffer.prototype = {
        push: function(cmd) {
            this.line.push(cmd);
        },
        cr: function() {
            this.script = this.script + this.line.join('; ');
            this.line = new Array();
            this.script = this.script + "\n";
        },
        close: function() {
            if (this.line.length > 0)
            {
                for (var i = 0; i < this.post_cmd.length; i++) {
                    this.push(pre_cmd[i]);
                }
                this.script = this.script + this.line.join('; ');
                line = null;
            }
        }

    };


    EJS.Compiler = function(source, left) {
        this.pre_cmd = ['var ___ViewO = [];'];
        this.post_cmd = new Array();
        this.source = ' ';
        if (source != null)
        {
            if (typeof source == 'string')
            {
                source = source.replace(/\r\n/g, "\n");
                source = source.replace(/\r/g, "\n");
                this.source = source;
            } else if (source.innerHTML) {
                this.source = source.innerHTML;
            }
            if (typeof this.source != 'string') {
                this.source = "";
            }
        }
        left = left || '<';
        var right = '>';
        switch (left) {
            case '[':
                right = ']';
                break;
            case '<':
                break;
            default:
                throw left + ' is not a supported deliminator';
                break;
        }
        this.scanner = new EJS.Scanner(this.source, left, right);
        this.out = '';
    };
    EJS.Compiler.prototype = {
        compile: function(options, name) {
            options = options || {};
            this.out = '';
            var put_cmd = "___ViewO.push(";
            var insert_cmd = put_cmd;
            var buff = new EJS.Buffer(this.pre_cmd, this.post_cmd);
            var content = '';
            var clean = function(content)
            {
                content = content.replace(/\\/g, '\\\\');
                content = content.replace(/\n/g, '\\n');
                content = content.replace(/"/g, '\\"');
                return content;
            };
            this.scanner.scan(function(token, scanner) {
                if (scanner.stag == null)
                {
                    switch (token) {
                        case '\n':
                            content = content + "\n";
                            buff.push(put_cmd + '"' + clean(content) + '");');
                            buff.cr();
                            content = '';
                            break;
                        case scanner.left_delimiter:
                        case scanner.left_equal:
                        case scanner.left_comment:
                            scanner.stag = token;
                            if (content.length > 0)
                            {
                                buff.push(put_cmd + '"' + clean(content) + '")');
                            }
                            content = '';
                            break;
                        case scanner.double_left:
                            content = content + scanner.left_delimiter;
                            break;
                        default:
                            content = content + token;
                            break;
                    }
                }
                else {
                    switch (token) {
                        case scanner.right_delimiter:
                            switch (scanner.stag) {
                                case scanner.left_delimiter:
                                    if (content[content.length - 1] == '\n')
                                    {
                                        content = chop(content);
                                        buff.push(content);
                                        buff.cr();
                                    }
                                    else {
                                        buff.push(content);
                                    }
                                    break;
                                case scanner.left_equal:
                                    buff.push(insert_cmd + "(EJS.Scanner.to_text(" + content + ")))");
                                    break;
                            }
                            scanner.stag = null;
                            content = '';
                            break;
                        case scanner.double_right:
                            content = content + scanner.right_delimiter;
                            break;
                        default:
                            content = content + token;
                            break;
                    }
                }
            });
            if (content.length > 0)
            {
                // Chould be content.dump in Ruby
                buff.push(put_cmd + '"' + clean(content) + '")');
            }
            buff.close();
            this.out = buff.script + ";";
            var to_be_evaled = '/*' + name + '*/this.process = function(_CONTEXT,_VIEW) { try { with(_VIEW) { with (_CONTEXT) {' + this.out + " return ___ViewO.join('');}}}catch(e){e.lineNumber=null;throw e;}};";

            try {
                eval(to_be_evaled);
            } catch (e) {
                if (typeof JSLINT != 'undefined') {
                    JSLINT(this.out);
                    for (var i = 0; i < JSLINT.errors.length; i++) {
                        var error = JSLINT.errors[i];
                        if (error.reason != "Unnecessary semicolon.") {
                            error.line++;
                            var e = new Error();
                            e.lineNumber = error.line;
                            e.message = error.reason;
                            if (options.view)
                                e.fileName = options.view;
                            throw e;
                        }
                    }
                } else {
                    throw e;
                }
            }
        }
    };


//type, cache, folder
    /**
     * Sets default options for all views
     * @param {Object} options Set view with the following options
     * <table class="options">
     <tbody><tr><th>Option</th><th>Default</th><th>Description</th></tr>
     <tr>
     <td>type</td>
     <td>'<'</td>
     <td>type of magic tags.  Options are '&lt;' or '['
     </td>
     </tr>
     <tr>
     <td>cache</td>
     <td>true in production mode, false in other modes</td>
     <td>true to cache template.
     </td>
     </tr>
     </tbody></table>
     * 
     */
    EJS.config = function(options) {
        EJS.cache = options.cache != null ? options.cache : EJS.cache;
        EJS.type = options.type != null ? options.type : EJS.type;
        EJS.ext = options.ext != null ? options.ext : EJS.ext;

        var templates_directory = EJS.templates_directory || {}; //nice and private container
        EJS.templates_directory = templates_directory;
        EJS.get = function(path, cache) {
            if (cache == false)
                return null;
            if (templates_directory[path])
                return templates_directory[path];
            return null;
        };

        EJS.update = function(path, template) {
            if (path == null)
                return;
            templates_directory[path] = template;
        };

        EJS.INVALID_PATH = -1;
    };
    EJS.config({cache: true, type: '<', ext: ''});



    /**
     * @constructor
     * By adding functions to EJS.Helpers.prototype, those functions will be available in the 
     * views.
     * @init Creates a view helper.  This function is called internally.  You should never call it.
     * @param {Object} data The data passed to the view.  Helpers have access to it through this._data
     */
    EJS.Helpers = function(data, extras) {
        this._data = data;
        this._extras = extras;
        extend(this, extras);
    };
    /* @prototype*/
    EJS.Helpers.prototype = {
        /**
         * Renders a new view.  If data is passed in, uses that to render the view.
         * @param {Object} options standard options passed to a new view.
         * @param {optional:Object} data
         * @return {String}
         */
        view: function(options, data, helpers) {
            if (!helpers)
                helpers = this._extras
            if (!data)
                data = this._data;
            return new EJS(options).render(data, helpers);
        },
        /**
         * For a given value, tries to create a human representation.
         * @param {Object} input the value being converted.
         * @param {Object} null_text what text should be present if input == null or undefined, defaults to ''
         * @return {String} 
         */
        to_text: function(input, null_text) {
            if (input == null || input === undefined)
                return null_text || '';
            if (input instanceof Date)
                return input.toDateString();
            if (input.toString)
                return input.toString().replace(/\n/g, '<br />').replace(/''/g, "'");
            return '';
        }
    };
    EJS.newRequest = function() {
        var factories = [function() {
                return new ActiveXObject("Msxml2.XMLHTTP");
            }, function() {
                return new XMLHttpRequest();
            }, function() {
                return new ActiveXObject("Microsoft.XMLHTTP");
            }];
        for (var i = 0; i < factories.length; i++) {
            try {
                var request = factories[i]();
                if (request != null)
                    return request;
            }
            catch (e) {
                continue;
            }
        }
    }

    EJS.request = function(path) {
        var request = new EJS.newRequest()
        request.open("GET", path, false);

        try {
            request.send(null);
        }
        catch (e) {
            return null;
        }

        if (request.status == 404 || request.status == 2 || (request.status == 0 && request.responseText == ''))
            return null;

        return request.responseText
    }
    EJS.ajax_request = function(params) {
        params.method = (params.method ? params.method : 'GET')

        var request = new EJS.newRequest();
        request.onreadystatechange = function() {
            if (request.readyState == 4) {
                if (request.status == 200) {
                    params.onComplete(request)
                } else
                {
                    params.onComplete(request)
                }
            }
        }
        request.open(params.method, params.url)
        request.send(null)
    }


})();